/*
 * Created on Oct 27, 2004
 */
package no.ntnu.fp.net.co;

import java.io.EOFException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

//import sun.reflect.generics.reflectiveObjects.NotImplementedException;

import no.ntnu.fp.net.admin.Log;
import no.ntnu.fp.net.cl.ClException;
import no.ntnu.fp.net.cl.ClSocket;
import no.ntnu.fp.net.cl.KtnDatagram;
import no.ntnu.fp.net.cl.KtnDatagram.Flag;

/**
 * Implementation of the Connection-interface. <br>
 * <br>
 * This class implements the behaviour in the methods specified in the interface
 * {@link Connection} over the unreliable, connectionless network realised in
 * {@link ClSocket}. The base class, {@link AbstractConnection} implements some
 * of the functionality, leaving message passing and error handling to this
 * implementation.
 * 
 * @author Sebj�rn Birkeland and Stein Jakob Nordb�
 * @see no.ntnu.fp.net.co.Connection
 * @see no.ntnu.fp.net.cl.ClSocket
 */
public class ConnectionImpl extends AbstractConnection {

    /** Keeps track of the used ports for each server port. */
    private static Map<Integer, Boolean> usedPorts = Collections.synchronizedMap(new HashMap<Integer, Boolean>());

    /**
     * Initialise initial sequence number and setup state machine.
     * 
     * @param myPort
     *            - the local port to associate with this connection
     */
    public ConnectionImpl(int myPort) {
//        throw new NotImplementedException();
    	super();
    	this.myPort = myPort;
    	this.myAddress = getIPv4Address();
    }

    private String getIPv4Address() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        }
        catch (UnknownHostException e) {
            return "127.0.0.1";
        }
    }

    /**
     * Establish a connection to a remote location.
     * 
     * @param remoteAddress
     *            - the remote IP-address to connect to
     * @param remotePort
     *            - the remote portnumber to connect to
     * @throws IOException
     *             If there's an I/O error.
     * @throws java.net.SocketTimeoutException
     *             If timeout expires before connection is completed.
     * @see Connection#connect(InetAddress, int)
     */
    public void connect(InetAddress remoteAddress, int remotePort) throws IOException,
            SocketTimeoutException {
    	this.remoteAddress = remoteAddress.getHostAddress();
    	this.remotePort = remotePort;
    	
    	KtnDatagram synPacket = constructInternalPacket(Flag.SYN);	//Creates a SYN-packet
    	KtnDatagram ackPacket;										//Declares the ACK-packet to be sent when receiving SYN-ACK from server
    	try {
			simplySendPacket(synPacket);							//Sends the SYN-packet
			this.state = State.SYN_SENT;
			
			KtnDatagram synAckPacket = receiveAck();				//Receive a SYN-ACK-packet
//			this.remoteAddress = synAckPacket.getSrc_addr();
//			this.remotePort = synAckPacket.getSrc_port();
			Thread.sleep(100);
			sendAck(synAckPacket, false);								
			lastValidPacketReceived = synAckPacket;			//Set last valid packet
			state = State.ESTABLISHED;							//Connection established
		} catch (ClException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			System.out.println("Can't send SYN-packet to server");
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			System.out.println("Thread error");
		}								
    		
    }

    /**
     * Listen for, and accept, incoming connections.
     * 
     * @return A new ConnectionImpl-object representing the new connection.
     * @see Connection#accept()
     */
    public Connection accept() throws IOException, SocketTimeoutException {
    	this.state = State.LISTEN;
    	KtnDatagram synPacket = null;			
    	
    	do synPacket = receivePacket(true);									//Receives a SYN-packet
    	while (!isValid(synPacket));
    
		try {
//			ConnectionImpl conn = new ConnectionImpl(myPort);				//Creates a new connection
//				conn.remoteAddress = synPacket.getSrc_addr();
//				conn.remotePort = synPacket.getSrc_port();
//				conn.nextSequenceNo = synPacket.getSeq_nr();
//				conn.lastValidPacketReceived = synPacket;
			remotePort = synPacket.getSrc_port();
			remoteAddress = synPacket.getSrc_addr();
			lastValidPacketReceived = synPacket;
			Thread.sleep(100);
			sendAck(synPacket, true);
			this.state = State.SYN_RCVD;
			
			KtnDatagram ackPacket = receiveAck();
				lastValidPacketReceived = ackPacket;
				state = State.ESTABLISHED;
			return this;
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	return null;
    }

    /**
     * Send a message from the application.
     * 
     * @param msg
     *            - the String to be sent.
     * @throws ConnectException
     *             If no connection exists.
     * @throws IOException
     *             If no ACK was received.
     * @see AbstractConnection#sendDataPacketWithRetransmit(KtnDatagram)
     * @see no.ntnu.fp.net.co.Connection#send(String)
     */
    public void send(String msg) throws ConnectException, IOException {
        KtnDatagram packetToSend = constructDataPacket(msg);
		try {
			Thread.sleep(200);
			sendDataPacketWithRetransmit(packetToSend);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    }

    /**
     * Wait for incoming data.
     * 
     * @return The received data's payload as a String.
     * @see Connection#receive()
     * @see AbstractConnection#receivePacket(boolean)
     * @see AbstractConnection#sendAck(KtnDatagram, boolean)
     */
    public String receive() throws ConnectException, IOException {
    	KtnDatagram receivedPacket = null;
    	do {
    		try {
    			receivedPacket = receivePacket(false);
    		} catch (EOFException e) {
    			try {
					Thread.sleep(100);
				} catch (InterruptedException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
    			sendAck(disconnectRequest, false);
    			state = State.CLOSE_WAIT;
    			throw e;
    		}
    	}
    	while(!isValid(receivedPacket));
    	
    	lastValidPacketReceived = receivedPacket;
    	sendAck(receivedPacket, false);
    	
    	return receivedPacket.getPayload().toString();
    }

    /**
     * Close the connection.
     * 
     * @see Connection#close()
     */
    public void close() throws IOException {
        KtnDatagram finClientPacket,finServerPacket;
        if(state == State.ESTABLISHED){						//Close called from client
        	finClientPacket = constructInternalPacket(Flag.FIN);
        	try {
        		Thread.sleep(100);
				simplySendPacket(finClientPacket);
				state = State.FIN_WAIT_1;
				
				receiveAck();
				state = State.FIN_WAIT_2;
				
				KtnDatagram receivedServerFinPacket;
				do receivedServerFinPacket = receivePacket(true);
				while(!isValid(receivedServerFinPacket));
				
				Thread.sleep(100);
				sendAck(receivedServerFinPacket, false);
				state = State.TIME_WAIT;
				
			} catch (ClException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
        } else if(state == State.CLOSE_WAIT){				//Close called from server
        	finServerPacket = constructInternalPacket(Flag.FIN);
        	try {
        		Thread.sleep(100);
				simplySendPacket(finServerPacket);
				state = State.LAST_ACK;
				
				receiveAck();
				state = State.CLOSED;
			} catch (ClException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
        }else{
        	throw new IOException("close() called in invalid state");
        }
       
    }

    /**
     * Test a packet for transmission errors. This function should only called
     * with data or ACK packets in the ESTABLISHED state.
     * 
     * @param packet
     *            Packet to test.
     * @return true if packet is free of errors, false otherwise.
     */
    protected boolean isValid(KtnDatagram packet) {
    	if(packet == null) return false;
    	if(state == State.LISTEN){
   			if(packet.getFlag() != Flag.SYN) return false;
   		}else if(state == State.ESTABLISHED){
   			
   			if(packet.getChecksum() != packet.calculateChecksum() || packet.getFlag() != Flag.NONE ) return false;
   			if(packet.getDest_port() != myPort || !packet.getDest_addr().equals(myAddress)) return false;
   		}
    	return true;
    }
}
